package com.magenta.maxoptra.gis.utils;

import com.magenta.commons.xml.XMLDateTime;
import com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendarImpl;
import org.joda.time.DateTime;

import javax.xml.datatype.XMLGregorianCalendar;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Logger;

/**
 *
 * @author Zaur Muhametgaleev
 * @version 20.01.2009 9:55:21
 * @since maxoptra-1.0
 */
public class DateTimeHelper {
    public static final String DATETIME_FORMAT_DEFAULT = "yyyyMMdd HH:mm:ss";
    public static final String DATETIME_FORMAT_DASHED = "yyyy-MM-dd HH:mm:ss";
    public static final String DATETIME_FORMAT_1 = "dd/MM/yyyy HH:mm";
    public static final String DATETIME_FORMAT_FULL = "dd/MM/yyyy HH:mm:ss";

    public static final String DATE_FORMAT_DEFAULT = "yyyyMMdd";
    public static final String DATE_FORMAT_DASHED = "yyyy-MM-dd";
    public static final String DATE_FORMAT_SLASHES = "dd/MM/yyyy";
    public static final String DATE_FORMAT_DOTTED = "dd.MM.yyyy";
    public static final String TIME_FORMAT_HMS_COLONS = "HH:mm:ss";
    public static final String TIME_FORMAT_DEFAULT = TIME_FORMAT_HMS_COLONS;

    public static final String SHORT_DATE = "ddMMM(HH:mm)";
    public static final String TIME_FORMAT_HOURS_MINUTES = "HHmm";
    public static final String TIME_FORMAT_HOURS_MINUTES_SEMICOLON = "HH:mm";
    public static final String DATETIME_FORMAT_WITHOUT_YEAR_AND_SECONDS = "dd/MM HH:mm";
    public static final String DAY_START_TIME = "00:00:00";
    public static final String DAY_END_TIME = "24:00:00";
    public static final TimeZone SAMARA_TIME_ZONE = TimeZone.getTimeZone("GMT+04");
//    public static final TimeZone SAMARA_TIME_ZONE = TimeZoneUtils.getServerTimezone();

    public static final long MSECS_IN_SECOND = 1000;
    public static final long SECS_IN_MINUTE = 60;
    public static final long MINS_IN_HOUR = 60;
    public static final long HOURS_IN_DAY = 24;
    public static final long MSECS_IN_MINUTE = MSECS_IN_SECOND * SECS_IN_MINUTE;
    public static final long MSECS_IN_HOUR = MSECS_IN_MINUTE * MINS_IN_HOUR;
    public static final long MSECS_IN_DAY = MSECS_IN_HOUR * HOURS_IN_DAY;
    public static final long MINS_IN_DAY = MINS_IN_HOUR * HOURS_IN_DAY;

    public static final int DAYS_IN_WEEK = 7;

    public static final double EPSILON = 0.000001;

    // shortenings of time in milliseconds
    static final long HOUR = MSECS_IN_HOUR;
    static final long MINUTE = MSECS_IN_MINUTE;
    static final long SECOND = MSECS_IN_SECOND;
    static final long DAY = MSECS_IN_DAY;

    public static final String[] DAY_OF_WEEK = {
            "",             // 0 is ""
            "SUNDAY",
            "MONDAY",
            "TUESDAY",
            "WEDNESDAY",
            "THURSDAY",
            "FRIDAY",
            "SATURDAY"
    };

    private static final Logger log = Logger.getLogger(DateTimeHelper.class.getName());
    private static final String HH_MM_PATTERN = "%02d:%02d";

    /**
     * Returns long representation of specified date. Time part of the date will be equal to 00:00:00.<br>
     * See {@link #convertToLong(String, String, String, String)} description for details.<br>
     *
     * @param date date to be parsed
     * @return parsed date
     */
    public static long convertDayToLong(String date) {
        return convertToLong(date, DAY_START_TIME);
    }

    /**
     * See {@link #convertToLong(String, String, String, String)} description for details.<br>
     * <br>
     * Date and time format must be following:<br>
     * 1) date format: {@link #DATE_FORMAT_DEFAULT}<br>
     * 2) time format: {@link #TIME_FORMAT_DEFAULT}<br>
     *
     * @param date date to be parsed
     * @param time time to be parsed
     * @return parsed and concatenated time
     */
    public static long convertToLong(String date, String time) {
        return convertToLong(date, time, null, null);
    }

    /**
     * Parses input date and time.<br>
     * Returns date with specified time.<br>
     * For example if <br>
     * o) <b>date=`18/02/2008`</b>; <br>
     * o) <b>time='12:32'</b><br>
     * then will be returned long representation of date <i><b>`18/02/2008 12:32`</b></i>
     *
     * @param date       date to be parsed
     * @param time       time to be parsed
     * @param dateFormat format of date
     * @param timeFormat format of time
     * @return parsed time
     */
    public static long convertToLong(String date, String time, String dateFormat, String timeFormat) {
        dateFormat = isDefaultNeeded(dateFormat) ? DATE_FORMAT_DEFAULT : dateFormat;
        timeFormat = isDefaultNeeded(timeFormat) ? TIME_FORMAT_DEFAULT : timeFormat;

        // for example if time is 914 (i.e. 9 hours 14 minutes) then it must be corrected like 0914 to be formatted
        if (timeFormat.equals(TIME_FORMAT_HOURS_MINUTES) && time.length() != 4) {// add zero-prefix if necessary
            time = "0" + time;
        }
        return parseDateTime(date, time, dateFormat + " " + timeFormat);
    }

    public static void correctPeriod(Date from, Date to) {
        if (from.before(to)) return;
        Calendar result = Calendar.getInstance();
        result.setTime(to);
        result.add(Calendar.DAY_OF_MONTH, 1);
        to.setTime(result.getTimeInMillis());
    }

    public static long parseDateTime(String date, String time, String dateTimeFormat) {
        final String dateTime = date + " " + time;
        return parseDateTime(dateTime, dateTimeFormat);
    }

    public static long parseDateTime(String dateTime, String dateTimeFormat) {
        SimpleDateFormat formatter = new SimpleDateFormat(dateTimeFormat);

        // parse date
        long parsedDate;
        try {
            parsedDate = formatter.parse(dateTime).getTime();
        } catch (ParseException e) {
            log.severe("Date must be in `" + dateTimeFormat + "` format");
            throw new RuntimeException(e);
        }
        return parsedDate;
    }

    public static String formatDateTime(Date date, String dateTimeFormat) {
        SimpleDateFormat formatter = new SimpleDateFormat(dateTimeFormat);
        return formatter.format(date);
    }

    /**
     * @param date date in {@link #DATETIME_FORMAT_DASHED} format
     * @return long representation of parsed date
     */
    public static long parseDateTimeDashed(String date) {
        return parseDateTime(date, DATETIME_FORMAT_DASHED);
    }

    /**
     * @param dateTime date and time in format {@link #DATETIME_FORMAT_1}
     * @return long representation of date and time
     */
    public static long parseDateTime(String dateTime) {
        return parseDateTime(dateTime, DATETIME_FORMAT_1);
    }

    private static boolean isDefaultNeeded(String str) {
        return str == null || str.equals("");
    }

    public static String convertDateToString(Date date, String resultFormat) {
        final SimpleDateFormat dateFormat = new SimpleDateFormat(resultFormat, Locale.UK);
        return dateFormat.format(date);
    }

    public static String convertDateLongToString(long date, String resultFormat) {
        return convertDateToString(new Date(date), resultFormat);
    }

    public static String convertLongToStringDefault(long date) {
        return convertDateToString(new Date(date), DATETIME_FORMAT_FULL);
    }

    public static String convertLongToShortDate(long date) {
        return convertDateToString(new Date(date), SHORT_DATE);
    }

    /**
     * Creates date object by string representation of this date
     *
     * @param dateStr datetime in following format #DATE_FORMAT_DEFAULT
     * @return date
     */
    public static Date createDate(String dateStr) {
        return new Date(DateTimeHelper.parseDateTime(dateStr, DateTimeHelper.DATE_FORMAT_DEFAULT));
    }

    public static int getWeekDay(Date date) {
        return getWeekDay(date.getTime());
    }

    public static int getWeekDay(long date) {
        Calendar cl = getCalendar(date);
        return cl.get(Calendar.DAY_OF_WEEK);
    }

    /**
     * Removes time part. If date = "18.01.2008 12:00:01" then time part will be cutted (result="18.01.2008 00:00:00").
     *
     * @param date date which time part must be cutted
     * @return date without time
     */
    public static long getDateWithoutTime(Date date) {
        return getDateWithoutTime(date.getTime());
    }

    /**
     * Adds or subtracts days from etalon date. If step is negative then subtraction will be done.
     *
     * @param date   date for which date offset must be calculated
     * @param offset days offset relative to current date
     * @return offseted date
     */
    public static long moveDay(long date, int offset) {
        Calendar cl = getCalendar(date);
        cl.add(Calendar.DAY_OF_YEAR, offset);
        return cl.getTimeInMillis();
    }

    public static long moveDay(Date date, int offset) {
        return moveDay(date.getTime(), offset);
    }

    public static long moveWeek(long date, int offset) {
        Calendar calendar = getCalendar(date);
        calendar.add(Calendar.WEEK_OF_YEAR, offset);
        return calendar.getTimeInMillis();
    }

    public static long moveWeek(Date date, int offset) {
        return moveWeek(date.getTime(), offset);
    }

    /**
     * See javadoc for {@link #getDateWithoutTime(java.util.Date)} method
     *
     * @param date date
     * @return -
     */
    public static long getDateWithoutTime(long date) {
        Calendar cl = getCalendar(date);
        cl.set(Calendar.HOUR_OF_DAY, 0);
        cl.set(Calendar.MINUTE, 0);
        cl.set(Calendar.SECOND, 0);
        cl.set(Calendar.MILLISECOND, 0);
        return cl.getTimeInMillis();
    }

    /**
     * Returns time part of date (ignoring day part).
     * For example if date is <i>"18 of January 2009 12:35"</i> then only time part (12:35 in milliseconds) will be
     * returned
     *
     * @param date date
     * @return - time part of date
     */
    public static long getTimeWithoutDate(long date) {
        Calendar cl = getCalendar(date);
        final int hours = cl.get(Calendar.HOUR_OF_DAY);
        final int minutes = cl.get(Calendar.MINUTE);
        final int seconds = cl.get(Calendar.SECOND);
        final int milliseconds = cl.get(Calendar.MILLISECOND);
        return hours * HOUR + minutes * MINUTE + seconds * SECOND + milliseconds;
    }

    /**
     * Check same calendar day for two dates.
     *
     * @param time1 one date.
     * @param time2 second date.
     * @return true if one date has a same calendar day as second date.
     */
    public static boolean haveSameDay(long time1, long time2) {
        Calendar cl = getCalendar(time1);
        int year1 = cl.get(Calendar.YEAR);
        int day1 = cl.get(Calendar.DAY_OF_YEAR);

        cl.setTimeInMillis(time2);
        int year2 = cl.get(Calendar.YEAR);
        int day2 = cl.get(Calendar.DAY_OF_YEAR);

        return year1 == year2 && day1 == day2;
    }

    /**
     * Check same calendar year for two dates.
     *
     * @param time1 one date.
     * @param time2 second date.
     * @return true if one date has a same calendar day as second date.
     */
    public static boolean haveSameYear(long time1, long time2) {
        Calendar cl = getCalendar(time1);
        int year1 = cl.get(Calendar.YEAR);
        cl.setTimeInMillis(time2);
        int year2 = cl.get(Calendar.YEAR);
        return year1 == year2;
    }

    /**
     * Set time for specified date. Time must be represented in format 'HH:mm'<br>
     * For example: date="12/01/09 14:12:04", HH=12, mm=35. Result: "12/01/09 12:35:00"
     *
     * @param date date which time must be set
     * @param time time that must be set
     * @return date with specified time
     */
    public static long setTime(long date, String time) {
        final String[] splittedTime = time.split(":");
        assert splittedTime.length == 2 : "Time must have following format: '" + TIME_FORMAT_HOURS_MINUTES_SEMICOLON + "' but was '" + time + "'";
        int hour = Integer.parseInt(splittedTime[0]);
        int minutes = Integer.parseInt(splittedTime[1]);

        Calendar calendar = getCalendar(date);
        calendar.set(Calendar.HOUR, hour);
        calendar.set(Calendar.MINUTE, minutes);
        return calendar.getTimeInMillis();
    }

    /**
     * Converts time represented by string to long value.
     *
     * @param time time in {@link #TIME_FORMAT_HMS_COLONS} format. For example time can have the value="10:30:11".
     * @return long value of input string
     * @throws java.text.ParseException -
     */
    public static long convertStringTimeToLong(String time) throws ParseException {
        String[] split = time.split(":");
        if (split.length != 3) {
            throw new ParseException("Time '" + time + "' is not correct. It must use the following format: " + TIME_FORMAT_HMS_COLONS, 0);
        }

        if (split[0].isEmpty() || split[0].length() > 2) {
            throw new ParseException("`Hours` value can't be empty or have length > 2", 0);
        }
        if (split[1].isEmpty() || split[1].length() > 2) {
            throw new ParseException("`Minutes` value can't be empty or have length > 2", 0);
        }
        if (split[2].isEmpty() || split[2].length() > 2) {
            throw new ParseException("`Seconds` value can't be empty or have length > 2", 0);
        }

        int hours = Integer.parseInt(split[0]);
        int minutes = Integer.parseInt(split[1]);
        int seconds = Integer.parseInt(split[2]);

        return hours * MSECS_IN_HOUR + minutes * MSECS_IN_MINUTE + seconds * MSECS_IN_SECOND;
    }

    public static int getDay(long time) {
        Calendar calendar = getCalendar(time);
        return calendar.get(Calendar.DAY_OF_MONTH);
    }

    public static int getYear(long time) {
        Calendar calendar = getCalendar(time);
        return calendar.get(Calendar.YEAR);
    }

    public static int getMonth(long time) {
        Calendar calendar = getCalendar(time);
        return calendar.get(Calendar.MONTH);
    }

    /**
     * Returns end of the day.<br>
     * For example if current time is '2009-01-14 15:00:03:014' the result will be '2009-01-14 23:59:59:999'
     *
     * @param time -
     * @return -
     */
    public static long getDayEnd(long time) {
        long date = getDateWithoutTime(time);
        return moveDay(date, 1) - 1;
    }

    public static long getDayStart(long time) {
        return getDateWithoutTime(time);
    }

    /**
     * Returns month offsetted relative to month of the specified date.
     * If offset is negative then previous month will be returned, otherwise - future month;
     *
     * @param date   offset will be applied relative to month of this date
     * @param offset number of monthes. If negative then previous month will be returned, otherwise - future.
     * @return month offsetted relative to current month
     */
    public static int moveMonth(long date, int offset) {
        final Calendar calendar = getCalendar(date);
        calendar.add(Calendar.MONTH, offset);
        return calendar.get(Calendar.MONTH);
    }

    public static Calendar getCalendar(Long time) {
        Calendar c = Calendar.getInstance();
        if (time != null) {
            c.setTimeInMillis(time);
        }
        return c;
    }

    public static Calendar getCalendar() {
        return getCalendar(null);
    }

    /**
     * Returns Monday day of week for specified date (time=00:00:00.000)
     *
     * @param date -
     * @return Monday day of week for specified date
     */
    public static long getWeekStart(long date) {
        return getDayStart(getWeekStartDay(date));
    }

    /**
     * Returns last day of week (SUNDAY, time=23:59:59.999)
     *
     * @param date -
     * @return -
     */
    public static long getWeekEnd(long date) {
        return getDayEnd(getWeekEndDay(date));
    }

    public static long getWeekStartDay(long date) {
        Calendar calendar = getCalendar(date);
        calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
        return calendar.getTimeInMillis();
    }

    public static long getWeekEndDay(long date) {
        Calendar calendar = getCalendar(date);
        calendar.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
        calendar.add(Calendar.DAY_OF_MONTH, 1);
        return DateTimeHelper.getDayEnd(calendar.getTimeInMillis());
    }

    public static long moveWeekAndGetWeekStart(long currentDate, int weekOffset) {
        long prevEndWeek = DateTimeHelper.moveWeek(currentDate, weekOffset);
        return DateTimeHelper.getWeekStart(prevEndWeek);
    }

    public static long moveWeekAndGetWeekEnd(long currentDate, int weekOffset) {
        long prevEndWeek = DateTimeHelper.moveWeek(currentDate, weekOffset);
        return DateTimeHelper.getWeekEnd(prevEndWeek);
    }

    public static long moveMonthAndGetMonthEnd(long time, int offset) {
        Calendar calendar = getCalendar(time);
        calendar.add(Calendar.MONTH, offset);
        return getMonthEnd(calendar.getTimeInMillis());
    }

    public static long moveMonthAndGetMonthStart(long time, int offset) {
        Calendar calendar = getCalendar(time);
        calendar.add(Calendar.MONTH, offset);
        return getMonthStart(calendar.getTimeInMillis());
    }

    public static long getMonthEnd(long time) {
        Calendar calendar = getCalendar(time);
        int maxDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
        calendar.set(Calendar.DAY_OF_MONTH, maxDay);
        return getDayEnd(calendar.getTimeInMillis());
    }

    /**
     * Returns beginning of the current month<br>
     * For example if the current time is '2009-01-14 15:00:03' then result will be '2009-01-01 00:00:00'
     *
     * @param time -
     * @return the beginning of the month of specified time
     */
    public static long getMonthStart(long time) {
        Calendar c = getCalendar(time);
        c.set(Calendar.DAY_OF_MONTH, 1);
        return getDateWithoutTime(c.getTimeInMillis());
    }


    public static Date convertXmlDateTimeToDate(String xmlDateTime) {
        try {
            return XMLDateTime.stringToDateTime(xmlDateTime);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * @param hours
     * @param minutes
     * @return
     */
    public static String formatDate4Sign(int hours, int minutes) {
        return String.format(HH_MM_PATTERN, hours, minutes);
    }

    /**
     * @param dateTime
     * @return
     */
    public static String formatDate4Sign(Date dateTime) {
        return String.format(HH_MM_PATTERN, dateTime.getHours(), dateTime.getMinutes());
    }

    /**
     * @param dateTime
     * @param timeZone
     * @return
     */
    public static boolean isDateToday(Date dateTime, TimeZone timeZone) {
        final DateTime startTime = new DateTime(dateTime);
        final Date currentDate = TimeZoneUtils.getTimeInTimeZone(new Date(), timeZone, true);
        final DateTime currentDateTime = new DateTime(currentDate);
        return (
                currentDateTime.dayOfMonth().equals(startTime.dayOfMonth()) &&
                        currentDateTime.monthOfYear().equals(startTime.monthOfYear()) &&
                        currentDateTime.year().equals(startTime.year())
        );
    }

    public static XMLGregorianCalendar getXmlGregorianCalendar(long time) {
        GregorianCalendar gCal = new GregorianCalendar();
        gCal.setTimeInMillis(time);
        XMLGregorianCalendar xmlGregorianCalendar = new XMLGregorianCalendarImpl(gCal);
        return xmlGregorianCalendar;
    }

    /**
     * Добавляет к указанной дате дополнительное смещение в милисекундах
     */
    public static Date addTimeMillisToDate(Date date, long additionalTime) {
        return new Date(date.getTime() + additionalTime);
    }
    
}
